﻿#region Copyright & License

// Copyright © 2012 François Chabot, Yves Dierick
// 
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// 
// http://www.apache.org/licenses/LICENSE-2.0
// 
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#endregion

using System;
using System.Diagnostics;
using System.Globalization;
using System.Text;
using System.Threading;
using Be.Stateless.Logging.Util;
using log4net.Appender;
using log4net.Core;
using log4net.Layout;
using log4net.Util;

namespace Be.Stateless.Logging.Appender
{
	/// <summary>
	/// This enumeration contains the different categories values that can be written in the category property of an event log entry.
	/// </summary>
	public enum CategoryLevel : short
	{
		Emergency = 20000,
		Fatal = 19900,
		Alert = 19800,
		Critical = 19700,
		Severe = 19600,
		Error = 19500,
		Warn = 19400,
		Notice = 19300,
		Info = 19200,
		Debug = 19100,
		Fine = 19000,
		Trace = 18900,
		Finer = 18800,
		Verbose = 18700,
		Finest = 18600
	}

	/// <summary>
	/// Writes events to the Windows event log. By default the <i>Application</i> log.
	/// </summary>
	/// <remarks>
	/// <para>
	/// The log4net levels are written in two locations of the Windows event log. The type and the category of a event log entry.
	/// The event log entry type give a partial information on the log4net level. This appender uses a many to one build-in mapping
	/// table. This mapping table is configured as follow.
	/// <list type="table">
	///   <listheader>
	///     <term>Log4Net Levels</term>
	///     <description>Event Log Entry Types</description>
	///   </listheader>
	///   <item>
	///     <term>Emergency, Fatal, Alert, Critical, Severe, Error</term>
	///     <description>Error</description>
	///   </item>
	///   <item>
	///     <term>Warn</term>
	///     <description>Warning</description>
	///   </item>
	///   <item>
	///     <term>Notice, Info, Debug, Fine, Trace, Finer, Verbose, Finest</term>
	///     <description>Information</description>
	///   </item>
	/// </list>
	/// The complete log4net level information is written in the category of a event log entry. Each log4net levels have 
	/// one identifier of type short integer, these values are definied in the next enumeration <see ref = "Be.Stateless.Logging.Appender.CategoryLevel" />.
	/// </para>
	/// <para>
	/// The event ID is used to identified the exception logged. The value of the event ID is equal 
	/// to a 16 bits hash code of the logged exception full name.
	/// The event log source can be set using the source property. By default the value is the application domain name.
	/// The category value is a short integer that identified the real log level 
	/// </para>
	/// </remarks>
	public class EventLogAppender : AppenderSkeleton
	{
		public EventLogAppender()
		{
			Name = DEFAULT_APPENDER_NAME;
			base.Layout = CreatePatternLayout(DEFAULT_LAYOUT_PATTERN);
		}

		public string MachineName
		{
			get { return _machineName; }
			set { _machineName = value; }
		}

		public string LogName
		{
			get { return _logName; }
			set { _logName = value; }
		}

		public string Source
		{
			get { return _source; }
			set { _source = value; }
		}

		public SecurityContext SecurityContext
		{
			get { return _securityContext ?? (_securityContext = SecurityContextProvider.DefaultProvider.CreateSecurityContext(this)); }
			set { _securityContext = value; }
		}

		protected override bool RequiresLayout
		{
			get { return false; }
		}

		#region Level Map/Mapping

		private static LevelMapping CreateLevelMapping()
		{
			var levelMapping = new LevelMapping();
			// mapped to error level
			levelMapping.Add(new EventLogMappingEntry(Level.Emergency, EventLogEntryType.Error));
			levelMapping.Add(new EventLogMappingEntry(Level.Fatal, EventLogEntryType.Error));
			levelMapping.Add(new EventLogMappingEntry(Level.Alert, EventLogEntryType.Error));
			levelMapping.Add(new EventLogMappingEntry(Level.Critical, EventLogEntryType.Error));
			levelMapping.Add(new EventLogMappingEntry(Level.Severe, EventLogEntryType.Error));
			levelMapping.Add(new EventLogMappingEntry(Level.Error, EventLogEntryType.Error));
			// mapped to warning level
			levelMapping.Add(new EventLogMappingEntry(Level.Warn, EventLogEntryType.Warning));
			// mapped to information level
			levelMapping.Add(new EventLogMappingEntry(Level.Notice, EventLogEntryType.Information));
			levelMapping.Add(new EventLogMappingEntry(Level.Info, EventLogEntryType.Information));
			levelMapping.Add(new EventLogMappingEntry(Level.Debug, EventLogEntryType.Information));
			levelMapping.Add(new EventLogMappingEntry(Level.Fine, EventLogEntryType.Information));
			levelMapping.Add(new EventLogMappingEntry(Level.Trace, EventLogEntryType.Information));
			levelMapping.Add(new EventLogMappingEntry(Level.Finer, EventLogEntryType.Information));
			levelMapping.Add(new EventLogMappingEntry(Level.Verbose, EventLogEntryType.Information));
			levelMapping.Add(new EventLogMappingEntry(Level.Finest, EventLogEntryType.Information));
			return levelMapping;
		}

		private static EventLogEntryType GetEntryType(Level level)
		{
			var type = (EventLogMappingEntry) _levelMapping.Lookup(level);
			return type.EventLogEntryType;
		}

		private static short GetCategoryLevelValue(Level level)
		{
			return (short) Enum.Parse(typeof(CategoryLevel), level.Name, true);
		}

		#endregion

		public override void ActivateOptions()
		{
			base.ActivateOptions();
			var sourceExists = EventLog.SourceExists(_source);

			using (SecurityContext.Impersonate(this))
			{
				if (!sourceExists)
				{
					InternalLog(string.Format("Creating event source [{0}] in log [{1}]", _source, _logName));
					CreateEventSource(_source, _logName);
				}
				else
				{
					var currentLogName = EventLog.LogNameFromSourceName(_source, _machineName);
					if (currentLogName != _logName)
					{
						InternalLog(string.Format("Changing event source [{0}] from log [{1}] to log [{2}]", _source, currentLogName, _logName));
						ChangeEventSource(_source, _logName);
					}
				}
			}
			_levelMapping.ActivateOptions();

			var patternLayout = Layout as PatternLayout;
			if (patternLayout != null)
				patternLayout.ActivateOptions();
			InternalLog(string.Format("Source [{0}] is registered to log [{1}]", _source, _logName));
		}

		protected override void Append(LoggingEvent loggingEvent)
		{
			try
			{
				var eventId = ((loggingEvent.ExceptionObject != null) ? GetEventId(loggingEvent.ExceptionObject) : 0);
				var entryType = GetEntryType(loggingEvent.Level);
				var message = RenderLoggingEvent(loggingEvent);
				string data = null;
				if (message.Length > MAX_MESSAGE_LENGTH)
				{
					data = message;
					message = string.Concat(message.Substring(0, MAX_MESSAGE_LENGTH - TRUNC_MESSAGE.Length), TRUNC_MESSAGE);
				}

				using (SecurityContext.Impersonate(this))
				{
					if (string.IsNullOrEmpty(data))
						EventLog.WriteEntry(Source, message, entryType, eventId, GetCategoryLevelValue(loggingEvent.Level));
					else
						EventLog.WriteEntry(Source, message, entryType, eventId, GetCategoryLevelValue(loggingEvent.Level), Encoding.Unicode.GetBytes(data));
				}
			}
			catch (Exception exception)
			{
				ErrorHandler.Error(string.Format("Unable to write to event log [{0}] using source [{1}].", LogName, Source), exception);
			}
		}

		protected override void OnClose()
		{
			base.OnClose();
			if (_reusableWriter != null)
				_reusableWriter.Dispose();
		}

		protected new string RenderLoggingEvent(LoggingEvent loggingEvent)
		{
			// Values from the base method, probably optimized.
			Writer.Reset(0x400, 0x100);

			// The base.RenderLoggingEvent contains an error in the conditional expression. In the base method if we ignore
			// the exceptions, these ones are even though rendered. The conditional code is not in the good branch of the
			// condition based on the IgnoresException boolean. That the reason why we override the code.
			Layout.Format(Writer, loggingEvent);
			return Writer.ToString();
		}

		#region Helpers

		private ReusableStringWriter Writer
		{
			get { return _reusableWriter ?? (_reusableWriter = new ReusableStringWriter(CultureInfo.InvariantCulture)); }
		}

		private static ILayout CreatePatternLayout(string pattern)
		{
			var layout = new PatternLayout(pattern);
			layout.AddConverter("exception", typeof(ExceptionPatternConverter));
			return layout;
		}

		private void ChangeEventSource(string source, string logName)
		{
			EventLog.DeleteEventSource(source, _machineName);
			CreateEventSource(source, logName);
		}

		private void CreateEventSource(string source, string logName)
		{
			var sourceData = new EventSourceCreationData(source, logName) { MachineName = _machineName };
			EventLog.CreateEventSource(sourceData);
		}

		private void InternalLog(string message)
		{
			LogLog.Debug(typeof(EventLogAppender), string.Format("{0}: {1}", GetType().FullName, message));
		}

		/// <summary>
		/// Computes a 16-bit hash of the exception type name to be used as log entry's event id
		/// </summary>
		/// <param name="ex"></param>
		/// <returns></returns>
		internal static ushort GetEventId(Exception ex)
		{
			// ReSharper disable AssignNullToNotNullAttribute
			if (ex == null)
				throw new ArgumentNullException("ex");
			return BitConverter.ToUInt16(LoggingHelper.Compute16BitsHashCode(Encoding.ASCII.GetBytes(ex.GetType().FullName)), 0);
			// ReSharper restore AssignNullToNotNullAttribute
		}

		#endregion

		// 32766, see http://msdn.microsoft.com/library/system.diagnostics.eventlog.writeentry
		// 31839, see ReportEvent (http://msdn.microsoft.com/en-us/library/windows/desktop/aa363679.aspx)
		internal const int MAX_MESSAGE_LENGTH = 31839; //32766;
		private const string TRUNC_MESSAGE = "[Truncated Message!]";
		private const string DEFAULT_APPENDER_NAME = "EventLogAppender";
		private const string DEFAULT_LAYOUT_PATTERN = "%message%newline%exception";

		private static readonly LevelMapping _levelMapping = CreateLevelMapping();
		private string _logName = string.Empty; // by default EventLog uses "Application" log.
		private string _machineName = ".";
		private ReusableStringWriter _reusableWriter; // IDisposable but the dispose method is empty.
		private SecurityContext _securityContext;
		private string _source = Thread.GetDomain().FriendlyName;

		#region Nested Class

		private class EventLogMappingEntry : LevelMappingEntry
		{
			public EventLogMappingEntry(Level level, EventLogEntryType entryType)
			{
				Level = level;
				_entryType = entryType;
			}

			public EventLogEntryType EventLogEntryType
			{
				get { return _entryType; }
			}

			private readonly EventLogEntryType _entryType;
		}

		#endregion
	}
}
